import { Buffer } from "buffer";
import { guid } from "./id.js";
import { walkJson } from "./index.js";
import { isPlainObject, isClass, isString } from "./is.js";

const RESULTCODE_SUCCESS = '0';
const MESSAGETYPE_HEADER = '0';
const MESSAGETYPE_CONTENT = '1';
const MESSAGETYPE_RESULT = '2';

const ENCODEBUFFER = "ENCODEBUFFER";
const ENCODEFILE = "ENCODEFILE";
const ENCODEBLOB = "ENCODEBLOB";
const ENCODEFORMDATA = "ENCODEFORMDATA";
const ENCODEERROR = "ENCODEERROR";
const ENCODERESPONSE = "ENCODERESPONSE";

const BufferUtils = {
    from(value) {
        return new BufferWrapper(value);
    },
    isTypedArray(value) {
        return value instanceof Int8Array || value instanceof Uint8Array || value instanceof Uint8ClampedArray ||
            value instanceof Int16Array || value instanceof Uint16Array || value instanceof Int32Array ||
            value instanceof Uint32Array || value instanceof Float32Array || value instanceof Float64Array
    },
    isBuffer(value) {
        return value instanceof ArrayBuffer;
    }
}

class BufferWrapper {
    constructor(value) {
        if (BufferUtils.isTypedArray(value)) {
            this._value = value;
        } else if (BufferUtils.isBuffer(value)) {
            this._value = new Uint8Array(value);
        } else if (isPlainObject(value) || Array.isArray(value)) {
            this._value = new TextEncoder().encode(JSON.stringify(value));
        } else if (isString(value)) {
            this._value = new TextEncoder().encode(value);
        } else {
            throw Error(`value must be TypedArray or ArrayBuffer or String or PlainObject or Array`);
        }
    }

    get typedArray() {
        return this._value;
    }

    get buffer() {
        return this._value.buffer;
    }

    toArray() {
        return Array.from(this._value);
    }

    toString() {
        return new TextDecoder().decode(this._value);
    }

    concat(...value) {
        let t = [this.typedArray, ...value.map(a => new BufferWrapper(a).typedArray)];
        let total = t.reduce((a, b) => a + b.buffer.byteLength, 0);
        return new BufferWrapper(Buffer.concat(t, total));
    }
}

export class BaseSerialize {
    constructor(context) {
        this._context = context;
    }

    get context() {
        return this._context;
    }

    type() { }

    isEncode(data) {
    }

    isDecode(data) {
        return data && isPlainObject(data) && Reflect.has(data, "encode") && data.encode === this.type();
    }

    async encode(data) { }

    async decode(data) { }

    async getBuffer(blob) {
        return new Promise((resolve, reject) => {
            let fr = new FileReader();
            fr.readAsArrayBuffer(blob);
            fr.onloadend = e => resolve(e.target.result);
            fr.onerror = e => reject(e);
        });
    }
}

class ErrorSerialize extends BaseSerialize {
    constructor(context) {
        super(context);
    }

    type() {
        return ENCODEERROR;
    }

    isEncode(data) {
        return data instanceof Error;
    }

    async encode(error) {
        let to = this.normalize(error);
        return {
            name: to.constructor.name,
            message: to.message,
            stack: to.stack
        };
    }

    decode(obj) {
        let t = new Error();
        Object.assign(t, { name: obj.name, message: obj.message, stack: obj.stack });
        return t;
    }

    normalize(error) {
        if (typeof error === "object" && error instanceof Error) {
            return error;
        } else if (typeof error === "string") {
            return new Error(error);
        }
        return new Error(error || 'unknow error');
    }
}

class BufferSerialize extends BaseSerialize {
    constructor(context) {
        super(context);
    }

    type() {
        return ENCODEBUFFER;
    }

    isEncode(value) {
        return BufferUtils.isTypedArray(value) || BufferUtils.isBuffer(value);
    }

    async encode(data) {
        return {
            type: data.constructor.name,
            data: Array.from(data)
        };
    }

    async decode({ type, data }) {
        if (type === 'ArrayBuffer') {
            return new Uint8Array(data).buffer;
        } else {
            return new window[type](data);
        }
    }
}

class BlobSerialize extends BaseSerialize {
    constructor(context) {
        super(context);
    }

    type() {
        return ENCODEBLOB;
    }

    isEncode(data) {
        return data instanceof Blob;
    }

    async encode(blob) {
        return {
            type: blob.type,
            ...this.context.appendStream(blob)
        };
    }

    async decode({ type, stream }) {
        let buffer = await Readable.fetch(stream);
        return new Blob([buffer], { type });
    }
}

class FileSerialize extends BaseSerialize {
    constructor(context) {
        super(context);
    }

    type() {
        return ENCODEFILE;
    }

    isEncode(data) {
        return data instanceof File;
    }

    async encode(file) {
        return {
            name: file.name,
            type: file.type,
            lastModified: file.lastModified,
            ...this.context.appendStream(file)
        };
    }

    async decode({ type, name, lastModified, stream }) {
        let buffer = await Readable.fetch(stream);
        return new File([buffer], name, {
            type,
            lastModified
        });
    }
}

class FormDataSerialize extends BaseSerialize {
    type() {
        return ENCODEFORMDATA;
    }

    isEncode(data) {
        return data instanceof FormData;
    }

    async encode(data) {
        let r = [], rr = {};
        data.forEach((value, key) => {
            r.push({ key, value });
        });
        await Promise.all(r.map(async ({ key, value }) => {
            rr[key] = await this.context.serializer.encode(value);
        }));
        return { data: rr };
    }

    async decode({ data }) {
        let r = new FormData(), rr = [];
        Reflect.ownKeys(data).forEach(key => rr.push({ key, value: data[key] }));
        await Promise.all(rr.map(async ({ key, value }) => {
            r.append(key, await this.context.serializer.decode(value));
        }));
        return r;
    }
}

class ResponseSerialize extends BaseSerialize {
    constructor(context) {
        super(context);
    }

    type() {
        return ENCODERESPONSE;
    }

    isEncode(data) {
        return data instanceof Response;
    }

    async encode(response) {
        let headers = {};
        [...response.headers.keys()].forEach((a) => headers[a] = response.headers.get(a));
        return {
            headers,
            status: response.status,
            statusText: response.statusText,
            ...this.context.appendStream(response.body)
        };
    }

    async decode({ headers, status, statusText, stream }) {
        return new Response(stream, {
            headers,
            status,
            statusText
        });
    }
}

const SerializeMap = [
    BufferSerialize,
    BlobSerialize,
    FileSerialize,
    ErrorSerialize,
    FormDataSerialize,
    ResponseSerialize
];

class SerializerContext {
    constructor(serializer, streams = {}) {
        this._streams = streams;
        this._serializer = serializer;
    }

    get streams() {
        return this._streams;
    }

    get serializer() {
        return this._serializer;
    }

    appendStream(stream) {
        let id = guid();
        this._streams[id] = stream;
        return {
            formatId: id,
            formated: 'stream',
        };
    }
}

class EncodeSerializer {
    constructor() {
        this._context = new SerializerContext(this);
        this._serializes = SerializeMap.map(a => {
            return new a(this._context);
        });
    }

    get context() {
        return this._context;
    }

    isEncode(data) {
        let t = this._serializes.find(a => a.isEncode(data));
        return t != undefined;
    }

    async encode(data) {
        let t = this._serializes.find(a => a.isEncode(data));
        if (t) {
            let r = await t.encode(data);
            r.encode = t.type();
            return r;
        }
        return data;
    }
}

class DecodeSerializer {
    constructor(streams = {}) {
        this._context = new SerializerContext(this, streams);
        this._serializes = SerializeMap.map(a => {
            return new a(this._context);
        });
    }

    get context() {
        return this._context;
    }

    isDecode(data) {
        let t = this._serializes.find(a => a.isDecode(data));
        return t != undefined;
    }

    async decode(data) {
        let t = this._serializes.find(a => a.isDecode(data));
        if (t) {
            if (data.formated && data.formated === 'stream' && this.context.streams[data.formatId]) {
                data.stream = this.context.streams[data.formatId];
            }
            return t.decode(data);
        }
        return data;
    }
}

export const JsonSerialize = {
    appendSerializer(serializerClass) {
        if (isClass(serializerClass) && serializerClass.prototype instanceof BaseSerialize) {
            SerializeMap.push(serializerClass);
        } else {
            throw Error(`param must be a class extends BaseSerialize`);
        }
    },
    async encode(data) {
        let serializer = new EncodeSerializer();
        let r = { __data__: data }, tasks = [];
        walkJson(r, null, null, (value, parent, key) => {
            if (serializer.isEncode(value)) {
                tasks.push(serializer.encode(value).then(s => {
                    if (parent) {
                        parent[key] = s;
                    }
                }));
                return false;
            }
            return true;
        });
        await Promise.all(tasks);
        return {
            result: r.__data__,
            streams: serializer.context.streams
        };
    },
    async decode(data, streams) {
        let serializer = new DecodeSerializer(streams);
        let r = { __data__: data }, tasks = [];
        walkJson(r, null, null, (value, parent, key) => {
            if (serializer.isDecode(value)) {
                tasks.push(serializer.decode(value).then(s => {
                    if (parent) {
                        parent[key] = s;
                    }
                }));
                return false;
            }
            return true;
        });
        await Promise.all(tasks);
        return r.__data__;
    }
}

export const PostMessageStream = {
    get readable() {
        let _controller = null;
        let _eventHandler = function ({ data }) {
            let { type, chunk } = data;
            if (type === 'stream') {
                _controller.enqueue(chunk);
            }
        }
        return new ReadableStream({
            start: (controller) => {
                _controller = controller;
                window.removeEventListener('message', _eventHandler);
                window.addEventListener('message', _eventHandler);
            },
            cancel() {
                window.removeEventListener('message', _eventHandler);
            }
        });
    },
    get writable() {
        return new WritableStream({
            write(chunk) {
                window.postMessage({ type: 'stream', chunk });
            }
        });
    }
}

export class PassThrough {
    stream() {
        return new TransformStream();
    }
}

export const Readable = {
    from(str) {
        let sended = false;
        return new ReadableStream({
            pull(controller) {
                if (!sended) {
                    controller.enqueue(BufferUtils.from(str).typedArray);
                    sended = true;
                }
                controller.close();
            }
        })
    },
    each(stream, fn) {
        return new Promise(resolve => {
            const reader = stream.getReader();
            function pump() {
                return reader.read().then(({ done, value }) => {
                    if (done) {
                        resolve();
                        return;
                    }
                    fn(value);
                    return pump();
                });
            }
            pump();
        });
    },
    fetch(stream) {
        return new Promise(resolve => {
            const reader = stream.getReader();
            let r = [];
            function pump() {
                return reader.read().then(({ done, value }) => {
                    if (done) {
                        resolve(Buffer.concat(r));
                        return;
                    }
                    r.push(BufferUtils.from(value).typedArray);
                    return pump();
                });
            }
            pump();
        });
    }
}

class Observer {
    constructor() {
        this._map = new Map();
    }

    subscribe(type, fn) {
        this._map.set(type, fn);
        return this;
    }

    publish(type, data) {
        if (this._map.has(type)) {
            this._map.get(type)(data);
            return true;
        }
        return false;
    }
}

class Request {
    constructor(id, params) {
        this._id = id;
        this._params = params;
        this._body = new PassThrough().stream();
        this._writer = this._body.writable.getWriter();
    }

    get id() {
        return this._id;
    }

    get body() {
        return this._body;
    }

    get params() {
        return this._params;
    }
};

class DataPacker {
    constructor(params) {
        this._id = guid().replace(/-/g, '');
        this._params = params;
        this._isSendHeader = false;
    }

    _pack(flag, contentBuffer) {
        let header = Buffer.alloc(4),
            length = contentBuffer.byteLength,
            id = Buffer.from(this._id);
        flag = Buffer.from(flag);
        header.writeInt32BE(length + 32 + 1);
        return Buffer.concat([header, flag, id, contentBuffer], length + 37);
    }

    _sendParams(controller) {
        if (!this._isSendHeader) {
            this._isSendHeader = true;
            controller.enqueue(this._pack(MESSAGETYPE_HEADER, BufferUtils.from(JSON.stringify(this._params)).typedArray));
        }
    }

    stream() {
        const ths = this;
        return new TransformStream({
            flush(controller) {
                ths._sendParams(controller);
                controller.enqueue(ths._pack(MESSAGETYPE_RESULT, BufferUtils.from(RESULTCODE_SUCCESS).typedArray));
            },
            transform(chunk, controller) {
                ths._sendParams(controller);
                controller.enqueue(ths._pack(MESSAGETYPE_CONTENT, chunk));
            }
        })
    }
}

class DataUnpacker extends Observer {
    constructor(readable) {
        super();
        this._before = null;
        this._map = new Map();
        Readable.each(readable, chunk => {
            this._get(BufferUtils.from(chunk).buffer);
        });
    }

    _decode(buffer) {
        let type = BufferUtils.from(buffer.slice(0, 1)).toString();
        let id = BufferUtils.from(buffer.slice(1, 33)).toString();
        let content = buffer.slice(33);
        if (type == MESSAGETYPE_HEADER) {
            let params = JSON.parse(BufferUtils.from(content).toString());
            if (!this._map.has(id)) {
                let request = new Request(id, params);
                this._map.set(id, request);
                this.publish('request', request);
            }
        } else if (type == MESSAGETYPE_CONTENT) {
            let request = this._map.get(id);
            if (request) {
                request._writer.write(BufferUtils.from(content).typedArray);
            }
        } else if (type == MESSAGETYPE_RESULT) {
            let request = this._map.get(id);
            if (request) {
                this._map.delete(id);
                request._writer.close();
            }
        }
        this.publish('unpackmessage', id);
    }

    _get(buffer) {
        if (this._before) {
            buffer = BufferUtils.from(this._before).concat(buffer).buffer;
        }
        if (buffer.byteLength > 4) {
            const BE = false;
            let start = 4, end = new DataView(buffer).getUint32(0, BE) + start;
            if (end > buffer.byteLength) {
                this._before = buffer;
            } else {
                this._decode(buffer.slice(start, end));
                this._before = null;
                this._get(buffer.slice(end));
            }
        } else {
            this._before = buffer;
        }
    }
}

class DataSenderImpl {
    constructor(writeable) {
        this._writeable = writeable;
        this._writer = writeable.getWriter();
    }

    send({ params, body }) {
        let r = this._getParams(params, body);
        return Readable.each(r.body.pipeThrough(new DataPacker(r.params).stream()), chunk => {
            this._writer.write(chunk);
        });
    }

    async sendOnce({ params, body }) {
        let r = this._getParams(params, body);
        return r.body.pipeThrough(new DataPacker(r.params).stream()).pipeTo(this._writeable);
    }

    _getParams(params, body) {
        if (!params) {
            params = {};
        }
        if (!body) {
            body = Readable.from('');
        }
        if (!body instanceof ReadableStream) {
            throw Error(`body must be a readable stream`);
        }
        return { params, body };
    }
}

class MessageSenderImpl {
    constructor(writable) {
        this._msg = new Map();
        this._sender = DataSender.from(writable);
    }

    async send(params) {
        let pid = guid();
        let { result, streams } = await JsonSerialize.encode(params);
        this._msg.set(pid, new Map());
        return this._sender.send({
            params: { id: pid, step: 'start', data: result, streams: Reflect.ownKeys(streams) }
        }).then(() => {
            let t = [];
            Reflect.ownKeys(streams).forEach(id => {
                t.push({ id, stream: streams[id] });
            });
            return Promise.all(t.map(({ id, stream }) => {
                return this._sender.send({
                    params: { id: pid, step: 'process', streamId: id },
                    body: stream instanceof ReadableStream ? stream : stream.stream()
                });
            })).then(() => {
                return this._sender.send({
                    params: { id: pid, step: 'end' }
                });
            });
        });
    }
}

class MessageReceiverImpl extends Observer {
    constructor(readable) {
        super();
        this._msg = new Map();
        this._receiver = DataReceiver.from(readable);
        this._receiver.subscribe('request', async ({ params, body }) => {
            let { step, data, streamId, streams } = params;
            if (step === 'start') {
                if (!this._msg.has(params.id)) {
                    let streamMap = new Map(), tt = {};
                    streams.forEach(stmId => {
                        let m = new PassThrough().stream();
                        streamMap.set(stmId, m);
                        tt[stmId] = m.readable;
                    });
                    this._msg.set(params.id, streamMap);
                    let result = await JsonSerialize.decode(data, tt);
                    this.publish('request', result);
                }
            } else if (step === 'process') {
                if (this._msg.has(params.id) && this._msg.get(params.id).has(streamId)) {
                    body.readable.pipeTo(this._msg.get(params.id).get(streamId).writable);
                }
            } else {
                if (this._msg.has(params.id)) {
                    this._msg.delete(params.id);
                }
            }
        });
    }
}

class MessageObserverImpl {
    constructor({ readable, writable }) {
        this._map = new Map();
        this._subscriber = new Map();
        this._sender = MessageSender.from(writable);
        this._receiver = MessageReceiver.from(readable);
        this._receiver.subscribe('request', async info => {
            let { id, code, type, error, params, data } = info;
            if (type) {
                if (this.isNotify(type, params)) {
                    Promise.resolve().then(() => this.notify(type, params)).then(data => {
                        this._sender.send({ id, code: 0, data });
                    }).catch(e => {
                        this._sender.send({ id, code: 1, error: e });
                    });
                }
                return;
            }
            if (code != undefined) {
                if (this._map.has(id)) {
                    let { resolve, reject } = this._map.get(id);
                    if (code == 0) {
                        resolve(data);
                    } else {
                        reject(error);
                    }
                }
            }
        });
    }

    isNotify(type, params) {
        return this._subscriber.has(type);
    }

    notify(type, params) {
        return this._subscriber.get(type)(params);
    }

    publish(type, params) {
        return new Promise((resolve, reject) => {
            let id = guid();
            this._sender.send({ id, type, params });
            this._map.set(id, { resolve, reject });
        });
    }

    subscribe(type, fn) {
        this._subscriber.set(type, fn);
        return this;
    }
}

export const DataSender = {
    from(writeable) {
        return new DataSenderImpl(writeable);
    }
}

export const DataReceiver = {
    from(readable) {
        return new DataUnpacker(readable);
    }
}

export const MessageSender = {
    from(writable) {
        return new MessageSenderImpl(writable);
    }
}

export const MessageReceiver = {
    from(readable) {
        return new MessageReceiverImpl(readable);
    }
}

export const MessageObserver = {
    from({ readable, writable }) {
        return new MessageObserverImpl({ readable, writable });
    }
}
